#ifndef GLOBALS_H
#define GLOBALS_H

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sstream>
#include <fstream>
#include <time.h>
#include <iostream>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <utility>
#include <signal.h>
#include <vector>
#include <list>
#include <utility>

#define DATASIZE 4096
#define SERVER_TIMEOUT 1
#define CLIENT_TIMEOUT 3*SERVER_TIMEOUT
const int FINAL_SEQNUM = -1;

// code provided from http://www.learncpp.com/cpp-tutorial/59-random-number-generation/
unsigned int generateRandomNumber()
{
    // our initial starting seed is 5323
    static unsigned int nSeed = 5323;
 
    // Take the current seed and generate a new value from it
    // Due to our use of large constants and overflow, it would be
    // very hard for someone to predict what the next number is
    // going to be from the previous one.
    nSeed = (8253729 * nSeed + 2396403);
 
    // Take the seed and return a value between 0 and 32767
    srand(time(NULL));
    return (rand() * nSeed) % 32767;
}
// end code from http://www.learncpp.com/cpp-tutorial/59-random-number-generation/

using namespace std;

/*
 *  Global variables for server and client connections
 *
 */

enum APPLICATION
{
	CLIENT=0,
	SERVER
};

enum PACKET_TYPE
{
	NONE = 0,
	DATA,
	ACK,
	FILE_REQUEST,
	ERROR,
	TRANSFER_COMPLETE,
	DISCONNECT
};

class CPacket
{
public:
	// public variables, should we change to private?
	unsigned short checksum;
	unsigned short filler; // used to ensure proper byte allignment
	PACKET_TYPE type;
	int seqnum;
	int length;
	char payload[DATASIZE];
	
	CPacket()
	: type(NONE), seqnum(0), checksum(0), filler(0), length(0)
	{
		memset(payload, 0, DATASIZE);
	}
	
	CPacket(const CPacket& other)
	: type(other.type), seqnum(other.seqnum), checksum(other.checksum), filler(other.filler), length(other.length)
	{
		memset(payload, 0, DATASIZE);
		memcpy(this->payload, other.payload, DATASIZE);
	}
	
	operator void*()
	{ return this; }
	
	bool operator==(CPacket& other)
	{ 
		bool ret = true;
		ret &= (other.type == type);
		ret &= (other.seqnum == seqnum);
		ret &= (other.checksum == checksum);
		return ret; 
	}
	
	static int PACKET_HEADER_SIZE;
	static int PACKET_SIZE;
	
	unsigned short computeChecksum()
	{
		memset(payload + this->length, 0, DATASIZE - this->length);
		
		// perform some algorithm to compute checksum
		unsigned short length = DATASIZE;
		unsigned short pad_bit = 0;
		unsigned long sum;
		
		//initialize sum to zero
		sum = 0;
		
		// compute sum over header fields
		sum += filler;
		sum += (unsigned long) (length);
		sum += (unsigned long) (seqnum);
        sum += (unsigned long) (type);
		
		// compute sum over payload
		for (int i = 0; i < length + pad_bit - 1; i = i + 2) 
		{
			sum += (unsigned long) ((payload[i] << 8) & 0xFF00) + (payload[i + 1] & 0xFF);// Append the sum
		}
		
		// Keep only the last 16 bits
		while (sum >> 16)
		{
			sum = (sum & 0xFFFF) + (sum >> 16);
		}
			
		// Take the one's complement of sum
		sum = ~sum;

		return ((unsigned short) sum);		
	}
};

int CPacket::PACKET_HEADER_SIZE = 2 * sizeof(unsigned short) + 3 * sizeof(int)  + sizeof(PACKET_TYPE);
int CPacket::PACKET_SIZE = CPacket::PACKET_HEADER_SIZE + (sizeof(char) * DATASIZE);

class IDataTransfer
{
public:
	IDataTransfer(APPLICATION app, int cwnd, double pl_ack, double pc_ack, 
				  double pl_data, double pc_data)
	: m_app(app),
	  m_cwnd(cwnd),
	  m_pl_ack(pl_ack),
	  m_pc_ack(pc_ack),
	  m_pl_data(pl_data),
	  m_pc_data(pc_data),
	  m_sockfd(0),
	  m_lastValidACK(0),
	  m_alarm_triggered(false), 
	  m_lastPacketValid(false),
	  m_fileRequestSent(false)
	  {
	  	m_addrlen = sizeof(struct sockaddr_in);
	  }
	  
	void newTransfer()
	{
		m_lastValidACK = 0;
		m_alarm_triggered = false; 
	  	m_lastPacketValid = false; 
	 	m_fileRequestSent = false;
	 	m_sendBuffer.clear();
	}
	
	virtual ~IDataTransfer()
	{
		close(m_sockfd);
	}
	
	bool isLastPacketValid() { return m_lastPacketValid; }
	
	virtual CPacket& lastrecv()
	{
		return m_recvBuffer;
	}
	
	virtual CPacket& pendingsend()
	{
		return m_temp_packet;
	}
	
	bool isDone()
	{
		recvpacket();
		
		while (!m_sendBuffer.empty() && m_sendBuffer.front().second == true)
		{
			// packet ACKed
			m_sendBuffer.pop_front();
		}
		
		return m_sendBuffer.empty();
	}
	
	void transferComplete()
	{
		if (m_app == SERVER)
		{
			// send file complete packet
			pendingsend().type = TRANSFER_COMPLETE;
			pendingsend().seqnum = FINAL_SEQNUM;
			pendingsend().length = 0;
			sendpacket();
		
			// wait for ACK packet
			while (!isDone())
			{}
			
			// send DISCONNECT packet, without requiring ACK
			pendingsend().type = DISCONNECT;
			pendingsend().seqnum = FINAL_SEQNUM;
			pendingsend().length = 0;
			sendpacket();
		}
		else // client
		{
			while (lastrecv().type != DISCONNECT)
			{
				// send ack for file complete packet
				pendingsend().type = ACK;
				pendingsend().seqnum = FINAL_SEQNUM;
				pendingsend().length = 0;
				sendpacket();
				
				// set up alarm, if expires server done sending
				alarm(CLIENT_TIMEOUT);
				
				// receive next packet
				recvpacket();
			}
		}
		
		alarm(0); // disable alarm from going off
		cout << "File transfer complete" << endl;
	}
	
	virtual int recvpacket()
	{
		m_lastPacketValid = false;
		
		// recieve packet
		int ret = recvfrom(m_sockfd, m_recvBuffer, CPacket::PACKET_SIZE, 0, (sockaddr*)&m_server_addr, &m_addrlen);
		if (ret == m_recvBuffer.length)
		{
			// received correct amt
		}
				
		cout << "Received packet with following information:" << endl;
		cout << "Type: ";
		if (m_recvBuffer.type == ACK)
		{
			cout << "ACK" << endl;
		}
		else
		{
			cout << "DATA" << endl;
		}
		cout << "Seqnum: " << m_recvBuffer.seqnum << endl;
		cout << "Length: " << m_recvBuffer.length << endl;
		
		if (m_app == CLIENT && !m_fileRequestSent)
		{
			// we got a packet from the server, means
			// file request packet arrived
			m_fileRequestSent = true;
			alarm(0);
		}
		
		unsigned short computedChecksum = m_recvBuffer.computeChecksum();
		// check if packet corrupted
		if (computedChecksum != m_recvBuffer.checksum)
		{
			cout << "CORRUPT PACKET" << endl;
			cout << "\tChecksum Header: " << m_recvBuffer.checksum << endl;
			cout << "\tComputed Checksum: " << computedChecksum << endl;
		}
		else // process uncorrupted packet
		{
			// if client, make sure correct seqnum
			if (m_app == CLIENT)
			{
				if (m_recvBuffer.seqnum == m_lastValidACK)
				{
					// set ACK for next packet
					m_lastValidACK = m_recvBuffer.seqnum + m_recvBuffer.length;
					m_lastPacketValid = true;
				}
				else if (m_recvBuffer.seqnum == FINAL_SEQNUM)
				{
					m_lastValidACK = FINAL_SEQNUM;
					m_lastPacketValid = true;
				}
			
				pendingsend().seqnum = m_lastValidACK;
			}
			else // if server, check seqnum of ACK
			{
				if (m_recvBuffer.type == ACK)
				{
					if (m_recvBuffer.seqnum == FINAL_SEQNUM)
					{
						// if we get this, we can ACK all packets in buffer
						for (std::list< std::pair<CPacket,bool> >::iterator it = m_sendBuffer.begin();
							 it != m_sendBuffer.end();
							 it++)
						{
							it->second = true;
						}
					}
					else
					{
						for (std::list< std::pair<CPacket,bool> >::iterator it = m_sendBuffer.begin();
							 it != m_sendBuffer.end();
							 it++)
						{
							int stored_seqnum = it->first.seqnum + it->first.length;
					
							if (stored_seqnum == m_recvBuffer.seqnum || m_lastValidACK > stored_seqnum)
							{
								it->second = true;
						
								if (stored_seqnum > m_lastValidACK)
								{
									m_lastValidACK = stored_seqnum;
								}
							}
						}
					}
				}
			}
		}
		
		cleanupCwnd();
		
		cout << endl;
		
		return ret;
	}
	
	virtual int sendpacket()
	{
		sendpacket(m_temp_packet, true);
	}
	
	virtual int sendpacket(CPacket& packet, bool addPacket)
	{
		int ret;
		
		// compute checksum before we intentially mess with packet
		packet.checksum = packet.computeChecksum();
		CPacket packetToSend(packet);
	
		// add new packet to send buffer for editing
		if (addPacket)
		{
			addSendPacket(packet);
		}
		
		if (m_app == SERVER && (m_sendBuffer.size() == 1 || m_alarm_triggered == true))
		{
			// if we are the server and this is the first item
			alarm(SERVER_TIMEOUT);
			m_alarm_triggered = false;
		}
		
		if (m_app == CLIENT && !m_fileRequestSent && packet.type == FILE_REQUEST)
		{
			alarm(CLIENT_TIMEOUT);
		}
		
		cout << "Sending packet with following information:" << endl;
		cout << "Type: ";
		if (packet.type == ACK)
		{
			cout << "ACK" << endl;
		}
		else
		{
			cout << "DATA" << endl;
		}
		cout << "Seqnum: " << packet.seqnum << endl;
		cout << "Length: " << packet.length << endl;
		
		if (processPacket(packetToSend))
		{
			int ret = sendto(m_sockfd, packetToSend, packet.length + CPacket::PACKET_HEADER_SIZE, 0, (sockaddr*)&m_server_addr, m_addrlen);
		}
			
		if (m_app == CLIENT && packet.type != FILE_REQUEST)
		{
			m_sendBuffer.pop_front();
		}
		
		cout << endl;
		
		return ret;
	}
	
	bool canSendNew()
	{
		cleanupCwnd();
		
		if (m_sendBuffer.size() < m_cwnd)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	
	void timerExpired()
	{
		if (m_app == SERVER)
		{
			cout << "TIMEOUT: Begin retransmission" << endl << endl;
			m_alarm_triggered = true;
		
			cleanupCwnd();
		
			for (std::list< std::pair<CPacket,bool> >::iterator it = m_sendBuffer.begin();
				 it != m_sendBuffer.end();
				 it++)
			{
				if (it->second == false)
				{
					// retransmit packet
					sendpacket(it->first, false);
				}
			}
		
			cout << "TIMEOUT: End retransmission" << endl << endl;
		
			m_alarm_triggered = false;
		}
		else // client
		{
			if (!m_fileRequestSent)
			{
				// retransmit packet
				sendpacket();
			}
			else
			{
				 // signal sent from file complete
			 	cout << "File transfer complete" << endl;
			 	exit(0);
			}
		}
	}
	
protected:
	std::list< std::pair<CPacket, bool> > m_sendBuffer;
	int m_sockfd;
	struct sockaddr_in m_server_addr, m_client_addr;
	socklen_t m_addrlen;
	
	CPacket m_recvBuffer;
	CPacket m_temp_packet;
	
	bool m_fileRequestSent;
	bool m_alarm_triggered;
	bool m_lastPacketValid;
	
	int m_cwnd;
	APPLICATION m_app;
	int m_lastValidACK;
	double m_pl_ack;
	double m_pc_ack;
	double m_pl_data;
	double m_pc_data;
	
private:
	void cleanupCwnd()
	{
		while (!m_sendBuffer.empty() && (m_sendBuffer.front().second == true || 
			   	(m_lastValidACK >= m_sendBuffer.front().first.seqnum + m_sendBuffer.front().first.length && m_sendBuffer.front().first.type == ACK)))
		{
			// packet ACKed
			m_sendBuffer.pop_front();
		}
	}

	void addSendPacket(CPacket& packet)
	{
	  	m_sendBuffer.push_back(std::pair<CPacket, bool>(packet, false));
	}

	void corruptPacket(CPacket& packet)
	{
		packet.filler = 1;
	}
	
	// will return true if we should send packet, else false
	// will perform corruption of packet if needed
	virtual bool processPacket(CPacket& packet)
	{
		double corrupt = (generateRandomNumber() % 100) / 100.0;
		double loss = (generateRandomNumber() % 100)  / 100.0;  // might be same as above
		
		double pl = packet.type == ACK ? m_pl_ack : m_pl_data;
		double pc = packet.type == ACK ? m_pc_ack : m_pc_data;
		
		// should we corrupt packet?
		if (corrupt < pc)
		{
			cout << "Packet is being corrupted before transmission" << endl;
			corruptPacket(packet);
		}
		
		// should we drop packet?
		if (loss < pl)
		{
			cout << "Packet is being intentionally dropped" << endl;
			return false;
		}
		else
		{
			return true;
		}
	}
};

/* Our signal handler.  We will tell the system to call this
function whenever it gets a SIGALRM */
void clientTimer(int sig);

class CClient : public IDataTransfer
{
public:
	CClient(std::string clientDirectory,
			char* server_hostname, 
			int server_portnum, 
			char* filename, 
			double pl_ack, 
			double pc_ack, 
			double pl_data, 
			double pc_data)
	: IDataTransfer(CLIENT, 0, pl_ack, pc_ack, pl_data, pc_data),
	  m_clientDirectory(clientDirectory),
	  m_server_hostname (server_hostname),
	  m_server_portnum(server_portnum),
	  m_filename(filename)
	{
		// create socket
		if ((m_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1)
		{
			perror("error: failed to create socket\n");
			exit(1);
		}
	
		// setup server socket info
		memset((void *) &m_server_addr, 0, sizeof(m_server_addr));
		m_server_addr.sin_family = AF_INET; 	/* host byte order */
		m_server_addr.sin_port = htons(m_server_portnum); /* short, network byte order */
	
		if ( inet_aton(m_server_hostname, &m_server_addr.sin_addr) == 0 )
		{
			perror("error: inet_aton() failed\n");
			exit(2);
		}
		
		signal(SIGALRM, clientTimer);
	}
	
private:
	std::string m_clientDirectory;
	char* m_server_hostname;
	int m_server_portnum;
	char* m_filename;
};

/* Our signal handler.  We will tell the system to call this
function whenever it gets a SIGALRM */
void serverTimer(int sig);

class CServer : public IDataTransfer
{
public:
	CServer(std::string serverDirectory, int portnum, int cwnd, double pl, double pc)
	: IDataTransfer(SERVER, cwnd,pl, pc, pl, pc),
	  m_serverDirectory(serverDirectory),
	  m_portnum(portnum)
	{	
		// create socket
		if ((m_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP))==-1)
		{
			perror("error: failed to create socket\n");
			exit(1);
		}
	
		// setup server socket info
		m_server_addr.sin_family = AF_INET; 	/* host byte order */
		m_server_addr.sin_port = htons(m_portnum); /* short, network byte order */
		m_server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
		/* INADDR_ANY allows clients to connect to any one of the host’s IP address */
		bzero(&(m_server_addr.sin_zero), 8); /* zero the struct */

		// bind socket
		if (bind(m_sockfd, (struct sockaddr *)&m_server_addr, sizeof(struct sockaddr)) == -1) 
		{
			perror("error: failed to bind socket\n");
			exit(1);
		}
		
		cout << "Server ready..." << endl;
		
		signal(SIGALRM, serverTimer);
	}
	
private:
	std::string m_serverDirectory;
	int m_portnum;
	char m_filename[DATASIZE];
};

CClient* client = NULL;
CServer* server = NULL;

/* Our signal handler.  We will tell the system to call this
function whenever it gets a SIGALRM */
void serverTimer(int sig)
{
	server->timerExpired();
}

/* Our signal handler.  We will tell the system to call this
function whenever it gets a SIGALRM */
void clientTimer(int sig)
{
	client->timerExpired();
}


#endif // GLOBALS_H

